package org.lazydoc.parser; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.NotEmpty; import org.lazydoc.annotation.*; import org.lazydoc.config.Config; import org.lazydoc.model.DocDataType; import org.lazydoc.model.DocEnum; import org.lazydoc.model.DocParameter; import org.lazydoc.model.DocProperty; import org.lazydoc.reporter.DocumentationReporter; import org.lazydoc.util.Inspector; import javax.validation.constraints.NotNull; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; import static org.apache.commons.lang3.StringUtils.removeEnd; public class DataTypeParser { private static final Logger log = LogManager.getLogger(DataTypeParser.class); private Map<String, DocDataType> dataTypes = new TreeMap<>(); private DocumentationReporter reporter; private Class<?> configuredBaseDTOClass = Object.class; private Config config; public DataTypeParser(DocumentationReporter reporter, Config config) { super(); this.config = config; this.reporter = reporter; if (StringUtils.isNotBlank(config.getBaseDTOClassname())) { try { this.configuredBaseDTOClass = Class.forName(config.getBaseDTOClassname()); } catch (ClassNotFoundException e) { log.warn("Could not find configured base DTO class " + config.getBaseDTOClassname()); } } } public void addListDataTypeStubAndAddRealDataType(DocParameter docParameter, Class<?> parameterType, String dataTypeName) { String parameterTypeName = removeEnd(removeEnd(parameterType.getSimpleName(), "[]"), config.getDataTypeSuffix()); String listDataTypeName = parameterTypeName + "List"; docParameter.setDataType(listDataTypeName); docParameter.setList(true); DocDataType dataType = new DocDataType(); dataType.setName(listDataTypeName); dataType.setList(true); DocProperty property = new DocProperty(); property.setDescription("List of " + parameterTypeName); property.setList(true); property.setType(dataTypeName); dataType.getProperties().add(property); dataTypes.put(dataType.getName(), dataType); addDataType(docParameter.getDataTypeClass().getComponentType()); } public void addDataType(Class<?> clazz) { if (isJavaType(clazz)) { return; } log.debug("Inspecting data type " + clazz.getName()); if (!isClassInstanceOfBaseVO(clazz) && isNotJavaType(clazz)) { throw new RuntimeException("Class " + clazz.getSimpleName() + " is not an inherited class of BaseVO"); } DocDataType dataType = new DocDataType(); dataType.setName(clazz.getSimpleName()); if (clazz.isAnnotationPresent(DescriptionAlias.class)) { dataType.setAlias(clazz.getAnnotation(DescriptionAlias.class).value()); } dataType.setNullValuesInSample(allowNullValuesInSample(clazz)); addDataType(clazz, dataType); dataTypes.put(dataType.getName(), dataType); } private void addDataType(Class<?> clazz, DocDataType dataType) { BeanInfo beanInfo; try { if (clazz == null) { return; } if (isSuperClassNotBaseVO(clazz)) { addDataType(getSuperClassOfVO(clazz), dataType); } beanInfo = Introspector.getBeanInfo(clazz); for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) { Class<?> propertyType = descriptor.getPropertyType(); Field propertyField = getPropertyField(clazz, descriptor); PropertyDescription propertyDescription = getPropertyDescription(propertyField, descriptor); if (skipThisField(propertyField, propertyDescription, descriptor) || propertyDescription == null) { continue; } DocProperty property = new DocProperty(); property.setName(getPropertyName(descriptor, propertyField)); property.setOrder(propertyDescription.order()); property.setRequired(isFieldRequired(propertyField, propertyDescription)); property.setRequest(isForRequest(propertyDescription, descriptor)); property.setResponse(isForResponse(propertyDescription, descriptor)); property.setDescription(getDescription(propertyField, property, propertyDescription)); property.setSample(getSample(propertyField, descriptor)); property.setDeprecated(isDeprecated(propertyField, descriptor)); property.setType(getPropertyType(propertyType, propertyField)); property.setList(Inspector.isListSetOrArray(propertyType)); property.setPrimitive(propertyType.isPrimitive()); property.setRequestNullValueSample(allowNullValueSample(descriptor.getWriteMethod())); property.setResponseNullValueSample(allowNullValueSample(descriptor.getReadMethod())); property.setEnumValues(getEnumValues(propertyType, propertyField, propertyDescription)); property.setAddEnumValues(propertyDescription.addPossibleEnumValues()); if (Inspector.isMap(propertyType)) { property.setMap(true); if (propertyField.isAnnotationPresent(PropertyMapDescription.class)) { PropertyMapDescription mapDescription = propertyField.getAnnotation(PropertyMapDescription.class); property.setMapKeyDescription(mapDescription.keyDescription()); property.setMapValueDescription(mapDescription.valueDescription()); } } dataType.getProperties().add(property); addFurtherVOClasses(propertyType); } if (clazz.isAnnotationPresent(JsonPropertyOrder.class)) { JsonPropertyOrder propertyOrder = clazz.getAnnotation(JsonPropertyOrder.class); int order = 1; for (String propertyName : propertyOrder.value()) { DocProperty property = getPropertyByName(propertyName, dataType.getProperties()); if (property != null) { property.setOrder(order++); } else { log.warn("Property " + propertyName + " not found in property list of type " + dataType.getName()); } } } Collections.sort(dataType.getProperties()); log.debug(dataType.toString()); } catch (IntrospectionException e) { throw new RuntimeException(e); } } private String getPropertyName(PropertyDescriptor descriptor, Field propertyField) { if (propertyField != null && propertyField.isAnnotationPresent(JsonProperty.class)) { return propertyField.getAnnotation(JsonProperty.class).value(); } return descriptor.getName(); } private boolean allowNullValueSample(Method method) { if (method != null && method.isAnnotationPresent(JsonSerialize.class)) { JsonSerialize jsonSerialize = method.getAnnotation(JsonSerialize.class); // TODO include modern way if (jsonSerialize.include().equals(JsonSerialize.Inclusion.NON_NULL)) { return false; } } return true; } private boolean allowNullValuesInSample(Class<?> clazz) { if (clazz != null && clazz.isAnnotationPresent(JsonSerialize.class)) { JsonSerialize jsonSerialize = clazz.getAnnotation(JsonSerialize.class); // TODO include modern way if (jsonSerialize.include().equals(JsonSerialize.Inclusion.NON_NULL)) { log.debug("Class " + clazz.getSimpleName() + " does not allow null value"); return false; } } return true; } private PropertyDescription getPropertyDescription(Method method) { return method != null ? method.getAnnotation(PropertyDescription.class) : null; } private DocProperty getPropertyByName(String propertyName, List<DocProperty> properties) { for (DocProperty property : properties) { if (property.getName().equals(propertyName)) { return property; } } return null; } private boolean isDeprecated(Field propertyField, PropertyDescriptor property) { if (propertyField != null && propertyField.isAnnotationPresent(Deprecated.class)) { return true; } boolean readMethodIsDeprecated = property.getReadMethod() != null && property.getReadMethod().isAnnotationPresent(Deprecated.class); boolean writeMethodIsDeprecated = property.getWriteMethod() != null && property.getWriteMethod().isAnnotationPresent(Deprecated.class); return readMethodIsDeprecated || writeMethodIsDeprecated; } private Set<DocEnum> getEnumValues(Class<?> propertyType, Field propertyField, PropertyDescription propertyDescription) { if(!propertyDescription.type().equals(void.class) && propertyDescription.type().isEnum()) { return getEnumList(propertyDescription.type()); } else if (propertyType.isEnum()) { return getEnumList(propertyType); } else if (Inspector.isListSetOrArray(propertyType)) { Class<?> genericClassOfList = Inspector.getGenericClassOfList(propertyType, propertyField.getGenericType()); if (genericClassOfList.isEnum()) { return getEnumList(genericClassOfList); } } return Collections.EMPTY_SET; } private Set<DocEnum> getEnumList(Class<?> propertyType) { Set<DocEnum> enumValues = new TreeSet<>(); for (Enum<?> enumElement : (Enum[]) propertyType.getEnumConstants()) { DocEnum docEnum = new DocEnum(); try { Field enumField = propertyType.getField(enumElement.toString()); if (enumField.isAnnotationPresent(EnumDescription.class)) { docEnum.setDescription(enumField.getAnnotation(EnumDescription.class).value()); } } catch (NoSuchFieldException e) { } docEnum.setValue(enumElement.toString()); enumValues.add(docEnum); } return enumValues; } private boolean isFieldRequired(Field propertyField, PropertyDescription propertyDescription) { if (propertyField != null && (propertyField.isAnnotationPresent(NotNull.class) || propertyField.isAnnotationPresent(NotEmpty.class) || propertyField.isAnnotationPresent(NotBlank.class))) { return true; } if (propertyDescription != null) { return propertyDescription.required(); } return false; } private boolean isSuperClassNotBaseVO(Class<?> clazz) { if (clazz.isArray()) { return clazz.getComponentType().equals(configuredBaseDTOClass); } return !clazz.getSuperclass().equals(configuredBaseDTOClass); } private String getPropertyType(Class<?> propertyType, Field propertyField) { if (propertyField != null) { PropertyDescription propertyDescription = getPropertyDescription(propertyField); if (propertyDescription != null && !propertyDescription.type().equals(void.class)) { addFurtherVOClasses(propertyDescription.type()); return propertyDescription.type().getSimpleName(); } if (Inspector.isListSetOrArray(propertyType)) { Class<?> genericClassOfList = Inspector.getGenericClassOfList(propertyType, propertyField.getGenericType()); if (genericClassOfList.isEnum()) { return "String"; } addFurtherVOClasses(genericClassOfList); return removeEnd(genericClassOfList.getSimpleName(), config.getDataTypeSuffix()); } } if (propertyType.isEnum()) { return "String"; } if (propertyType.getSimpleName().equals("BigDecimal")) { return "Number"; } if (propertyType.getSimpleName().equals("Integer")) { return "Number (int)"; } return propertyType.getSimpleName(); } private String[] getSample(Field propertyField, PropertyDescriptor descriptor) { if (propertyField != null && propertyField.isAnnotationPresent(Sample.class)) { return propertyField.getAnnotation(Sample.class).value(); } Method readMethod = descriptor.getReadMethod(); Method writeMethod = descriptor.getWriteMethod(); if (readMethod != null && readMethod.isAnnotationPresent(Sample.class)) { return readMethod.getAnnotation(Sample.class).value(); } if (writeMethod != null && writeMethod.isAnnotationPresent(Sample.class)) { return writeMethod.getAnnotation(Sample.class).value(); } return new String[]{}; } private boolean isForRequest(PropertyDescription propertyDescription, PropertyDescriptor descriptor) { if (descriptor.getWriteMethod() != null && descriptor.getWriteMethod().isAnnotationPresent(JsonIgnore.class)) { return false; } return (!propertyDescription.onlyRequest() && !propertyDescription.onlyResponse()) || propertyDescription.onlyRequest(); } private boolean isForResponse(PropertyDescription propertyDescription, PropertyDescriptor descriptor) { if (descriptor.getReadMethod() != null && descriptor.getReadMethod().isAnnotationPresent(JsonIgnore.class)) { return false; } return (!propertyDescription.onlyRequest() && !propertyDescription.onlyResponse()) || propertyDescription.onlyResponse(); } private String getDescription(Field propertyField, DocProperty property, PropertyDescription propertyDescription) { String description = ""; if (propertyField != null) { if (propertyField.isAnnotationPresent(PropertyDescription.class)) { reporter.addDocumentedField(propertyField.getDeclaringClass(), propertyField.getName()); description = propertyDescription.description(); } else { reporter.addUndocumentedField(propertyField.getDeclaringClass(), propertyField.getName()); } } else { description = propertyDescription.description(); } return description; } private PropertyDescription getPropertyDescription(Field propertyField) { return propertyField.getAnnotation(PropertyDescription.class); } private Field getPropertyField(Class<?> clazz, PropertyDescriptor descriptor) { try { for (Field field : clazz.getDeclaredFields()) { if (field.getName().equals(descriptor.getName())) { return field; } } } catch (Exception e) { log.debug(e.getMessage()); } if (!descriptor.getName().equals("class")) { log.debug("ERROR: Could not find field for descriptor " + descriptor.getName() + " in class " + clazz.getSimpleName()); } return null; } private void addFurtherVOClasses(Class<?> voClass) { if (isClassInstanceOfBaseVO(voClass)) { addDataType(voClass); } } private boolean isClassInstanceOfBaseVO(Class<?> voClass) { boolean isInstanceOf = configuredBaseDTOClass.isAssignableFrom(voClass); if (!isInstanceOf && isNotJavaType(voClass)) { if (Inspector.isListSetOrArray(voClass)) { return configuredBaseDTOClass.isAssignableFrom(Inspector.getGenericClassOfList(voClass, voClass)); } } return isInstanceOf; } private boolean isNotJavaType(Class<?> clazz) { return !isJavaType(clazz); } private boolean isJavaType(Class<?> clazz) { return clazz.getName().startsWith("java.") || clazz.isEnum() || clazz.equals(void.class) || clazz.isPrimitive(); } private Class<?> getSuperClassOfVO(Class<?> clazz) { if (clazz.isArray()) { return clazz.getComponentType().getSuperclass(); } return clazz.getSuperclass(); } private boolean skipThisField(Field propertyField, PropertyDescription propertyDescription, PropertyDescriptor descriptor) { if (propertyField != null && propertyField.isAnnotationPresent(IgnoreForDocumentation.class)) { reporter.addIgnoredField(propertyField.getDeclaringClass(), propertyField.getName()); return true; } if (propertyDescription == null && propertyField != null) { Method readMethod = descriptor.getReadMethod(); Method writeMethod = descriptor.getWriteMethod(); if (readMethod != null && readMethod.isAnnotationPresent(JsonIgnore.class)) { if (writeMethod != null && writeMethod.isAnnotationPresent(JsonProperty.class)) { reporter.addUndocumentedField(readMethod != null ? readMethod.getDeclaringClass() : writeMethod.getDeclaringClass(), descriptor.getName()); } else { reporter.addIgnoredField(readMethod != null ? readMethod.getDeclaringClass() : writeMethod.getDeclaringClass(), descriptor.getName()); } } else { reporter.addUndocumentedField(readMethod != null ? readMethod.getDeclaringClass() : writeMethod.getDeclaringClass(), descriptor.getName()); } return true; } return false; } private PropertyDescription getPropertyDescription(Field propertyField, PropertyDescriptor descriptor) { PropertyDescription propertyDescription = null; if (propertyField == null) { Method readMethod = descriptor.getReadMethod(); propertyDescription = getPropertyDescription(readMethod); if (propertyDescription == null) { propertyDescription = getPropertyDescription(descriptor.getWriteMethod()); if (propertyDescription != null) { reporter.addDocumentedMethod(descriptor.getWriteMethod().getDeclaringClass(), descriptor.getName()); return propertyDescription; } } else { reporter.addDocumentedMethod(readMethod.getDeclaringClass(), descriptor.getName()); return propertyDescription; } } else { propertyDescription = getPropertyDescription(propertyField); if (propertyDescription != null) { reporter.addDocumentedField(propertyField.getDeclaringClass(), propertyField.getName()); return propertyDescription; } } return null; } public Map<String, DocDataType> getDataTypes() { return dataTypes; } }